iT邦幫忙

2023 iThome 鐵人賽

DAY 13
1
Security

Windows Security 101系列 第 13

[Day13] Dark LoadLibrary

  • 分享至 

  • xImage
  •  

這篇研究是 MDSec 在 2021 年發表的文章,作者實作的 Dark LoadLibrary 可以說是 Reflective Loader 的進化版,在 DLL 載入時 Linking Internal Structures 的部分做的更加完整,但也還是有未完成的部分。我想透過分析程式碼,分享作者在設計 DLL Loading 的部分做了哪些改良 。

Dark LoadLibrary

首先,作者給了一張圖比較了 Reflective Loader 和 Dark LoadLibrary 之間的差異。

https://ithelp.ithome.com.tw/upload/images/20230927/20120098wc66l2rXOz.jpg

(ref: https://www.mdsec.co.uk/2021/06/bypassing-image-load-kernel-callbacks/)

圖中可以發現,雖然 Reflective Loader 可以繞過 LoadImageNotifyRoutine,但是在後續沒辦法對載入的 module 操作使用 GetProcAddress 和 GetModuleHandle,載入後的 DLL 也和一般 LoadLibrary 載入的 DLL 不同。

所以其實作者的目標是做出真正的 DLL Loader。

根據原文,DarkLoadLibrary 可以分解成幾個步驟:

1. Make sure the data you’re about to load is a valid PE

一開始會使用這兩個 function 先取得必要的 APIs

  • IsModulePresent
    • 從 PEB→Ldr→InMemoryOrderModuleList 中檢查 DLL 的名稱來判斷是否已被載入
  • GetFunctionAddress
    • 從 NT Header→OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT] 中尋找 exported function address。
    • 找的方法和 Reflective Loader 一樣,先確定 AddressOfNameOrdinals 中的 ordinal,再以此去 AddressOfFunctions 中查找

DarkLoadLibrary 有兩種 flag 可以設定:

  • LOAD_LOCAL_FILE
  • LOAD_MEMORY

結果都是把整個 DLL 讀進 buffer 中存放,之後便會檢查:

  1. DLL 是否已被載入
  2. buffer 中的 PE 格式是否正確
PDARKMODULE DarkLoadLibrary(
	DWORD   dwFlags,
	LPCWSTR lpwBuffer,
	LPVOID	lpFileBuffer,
	DWORD   dwLen,
	LPCWSTR lpwName
)
{
	HEAPALLOC pHeapAlloc = (HEAPALLOC)GetFunctionAddress(IsModulePresent(L"Kernel32.dll"), "HeapAlloc");
	GETPROCESSHEAP pGetProcessHeap = (GETPROCESSHEAP)GetFunctionAddress(IsModulePresent(L"Kernel32.dll"), "GetProcessHeap");

	/*
		TODO:
		I would really love to stop using error messages that need this.
		All the other safe versions of wsprintfW are located in the CRT,
			which is an issue if there is no CRT in the process.

		For now let us hope nobody will pass a name larger than 500 bytes. :/
	*/
	WSPRINTFW pwsprintfW = (WSPRINTFW)GetFunctionAddress(IsModulePresent(L"User32.dll"), "wsprintfW");

	PDARKMODULE dModule = (DARKMODULE*)pHeapAlloc(pGetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(DARKMODULE));
	if (!dModule)
		return NULL;

	dModule->bSuccess = FALSE;
	dModule->bLinkedToPeb = TRUE;

	// get the DLL data into memory, whatever the format it's in
	switch (LOWORD(dwFlags))
	{
	case LOAD_LOCAL_FILE:
		if (!ParseFileName(dModule, lpwBuffer) || !ReadFileToBuffer(dModule))
		{
			goto Cleanup;
		}
		break;

	case LOAD_MEMORY:
		dModule->dwDllDataLen = dwLen;
		dModule->pbDllData = lpFileBuffer;

		/*
			This is probably a hack for the greater scheme but lol
		*/
		dModule->CrackedDLLName = lpwName;
		dModule->LocalDLLName = lpwName;

		if (lpwName == NULL)
			goto Cleanup;

		break;

	default:
		break;
	}

	if (dwFlags & NO_LINK)
		dModule->bLinkedToPeb = FALSE;

	// is there a module with the same name already loaded
	if (lpwName == NULL)
	{
		lpwName = dModule->CrackedDLLName;
	}

	HMODULE hModule = IsModulePresent(lpwName);

	if (hModule != NULL)
	{
		dModule->ModuleBase = (ULONG_PTR)hModule;
		dModule->bSuccess = TRUE;

		goto Cleanup;
	}

	// make sure the PE we are about to load is valid
	if (!IsValidPE(dModule->pbDllData))
	{
		dModule->ErrorMsg = (wchar_t*)pHeapAlloc(pGetProcessHeap(), HEAP_ZERO_MEMORY, 500);
		if (!dModule->ErrorMsg)
			goto Cleanup;

		pwsprintfW(dModule->ErrorMsg, TEXT("Data is an invalid PE: %s"), lpwName);
		goto Cleanup;
	}

2. Copy headers and sections into memory, setting the correct memory permissions

這邊的流程其實跟 Reflective Loader 差不多,分配 NtHeaders->OptionalHeader.SizeOfImage 大小的空間,再將 section 複製到對應的位置

// map the sections into memory
	if (!MapSections(dModule))
	{
		dModule->ErrorMsg = (wchar_t*)pHeapAlloc(pGetProcessHeap(), HEAP_ZERO_MEMORY, 500);
		if (!dModule->ErrorMsg)
			goto Cleanup;

		pwsprintfW(dModule->ErrorMsg, TEXT("Failed to map sections: %s"), lpwName);
		goto Cleanup;
	}

2.a If necessary perform relocation’s on the image base

如果有 relocation 的話,也會和 Reflective Loader 一樣修正 relocation

3. Resolve both the import tables

這個步驟會解析一些該 DLL 的 Import table,並載入相依的 DLL 和其中的 imported functions。

	// handle the import tables
	if (!ResolveImports(dModule))
	{
		dModule->ErrorMsg = (wchar_t*)pHeapAlloc(pGetProcessHeap(), HEAP_ZERO_MEMORY, 500);
		if (!dModule->ErrorMsg)
			goto Cleanup;

		pwsprintfW(dModule->ErrorMsg, TEXT("Failed to resolve imports: %s"), lpwName);
		goto Cleanup;
	}

4. Link to PEB

最後就是將 DLL link 到 PEB,這也是作者文章中介紹的主要內容。

	// link the module to the PEB
	if (dModule->bLinkedToPeb)
	{
		if (!LinkModuleToPEB(dModule))
		{
			dModule->ErrorMsg = (wchar_t*)pHeapAlloc(pGetProcessHeap(), HEAP_ZERO_MEMORY, 500);
			if (!dModule->ErrorMsg)
				goto Cleanup;
			
			pwsprintfW(dModule->ErrorMsg, TEXT("Failed to link module to PEB: %s"), lpwName);
			goto Cleanup;
		}
	}

LinkModuleToPEB 的目的是將載入的 DLL 加入 InLoadOrderModuleListInMemoryOrderModuleList , InInitializationOrderModuleList

首先建立一塊 LDR_DATA_TABLE_ENTRY 的結構,並且填入 DLL 相關的資訊。

BOOL LinkModuleToPEB(
	PDARKMODULE pdModule
)
{
	PIMAGE_NT_HEADERS pNtHeaders;
	UNICODE_STRING FullDllName, BaseDllName;
	PLDR_DATA_TABLE_ENTRY2 pLdrEntry = NULL;

	GETPROCESSHEAP pGetProcessHeap = (GETPROCESSHEAP)GetFunctionAddress(IsModulePresent(L"Kernel32.dll"), "GetProcessHeap");
	HEAPALLOC pHeapAlloc = (HEAPALLOC)GetFunctionAddress(IsModulePresent(L"Kernel32.dll"), "HeapAlloc");
	RTLINITUNICODESTRING pRtlInitUnicodeString = (RTLINITUNICODESTRING)GetFunctionAddress(IsModulePresent(L"ntdll.dll"), "RtlInitUnicodeString");
	NTQUERYSYSTEMTIME pNtQuerySystemTime = (NTQUERYSYSTEMTIME)GetFunctionAddress(IsModulePresent(L"ntdll.dll"), "NtQuerySystemTime");

	pNtHeaders = RVA(
		PIMAGE_NT_HEADERS, 
		pdModule->pbDllData, 
		((PIMAGE_DOS_HEADER)pdModule->pbDllData)->e_lfanew
	);

	// convert the names to unicode
	pRtlInitUnicodeString(
		&FullDllName, 
		pdModule->LocalDLLName
	);

	pRtlInitUnicodeString(
		&BaseDllName, 
		pdModule->CrackedDLLName
	);

	// link the entry to the PEB
	pLdrEntry = (PLDR_DATA_TABLE_ENTRY2)pHeapAlloc(
		pGetProcessHeap(),
		HEAP_ZERO_MEMORY,
		sizeof(LDR_DATA_TABLE_ENTRY2)
	);

	if (!pLdrEntry)
	{
		return FALSE;
	}

	// start setting the values in the entry
	pNtQuerySystemTime(&pLdrEntry->LoadTime);

	// do the obvious ones
	pLdrEntry->ReferenceCount        = 1;
	pLdrEntry->LoadReason            = LoadReasonDynamicLoad;
	pLdrEntry->OriginalBase          = pNtHeaders->OptionalHeader.ImageBase;

	// set the hash value
	pLdrEntry->BaseNameHashValue = LdrHashEntry(
		BaseDllName,
		FALSE
	);

	// correctly add the base address to the entry
	AddBaseAddressEntry(
		pLdrEntry,
		(PVOID)pdModule->ModuleBase
	);

	// and the rest
	pLdrEntry->ImageDll              = TRUE;
	pLdrEntry->LoadNotificationsSent = TRUE; // lol
	pLdrEntry->EntryProcessed        = TRUE;
	pLdrEntry->InLegacyLists         = TRUE;
	pLdrEntry->InIndexes             = TRUE;
	pLdrEntry->ProcessAttachCalled   = TRUE;
	pLdrEntry->InExceptionTable      = FALSE;
	pLdrEntry->DllBase               = (PVOID)pdModule->ModuleBase;
	pLdrEntry->SizeOfImage           = pNtHeaders->OptionalHeader.SizeOfImage;
	pLdrEntry->TimeDateStamp         = pNtHeaders->FileHeader.TimeDateStamp;
	pLdrEntry->BaseDllName           = BaseDllName;
	pLdrEntry->FullDllName           = FullDllName;
	pLdrEntry->ObsoleteLoadCount     = 1;
	pLdrEntry->Flags                 = LDRP_IMAGE_DLL | LDRP_ENTRY_INSERTED | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED;

	// set the correct values in the Ddag node struct
	pLdrEntry->DdagNode = (PLDR_DDAG_NODE)pHeapAlloc(
		pGetProcessHeap(),
		HEAP_ZERO_MEMORY,
		sizeof(LDR_DDAG_NODE)
	);

	if (!pLdrEntry->DdagNode)
	{
		return 0;
	}

	pLdrEntry->NodeModuleLink.Flink    = &pLdrEntry->DdagNode->Modules;
	pLdrEntry->NodeModuleLink.Blink    = &pLdrEntry->DdagNode->Modules;
	pLdrEntry->DdagNode->Modules.Flink = &pLdrEntry->NodeModuleLink;
	pLdrEntry->DdagNode->Modules.Blink = &pLdrEntry->NodeModuleLink;
	pLdrEntry->DdagNode->State         = LdrModulesReadyToRun;
	pLdrEntry->DdagNode->LoadCount     = 1;

	// add the hash to the LdrpHashTable
	AddHashTableEntry(
		pLdrEntry
	);

	// set the entry point
	pLdrEntry->EntryPoint = RVA(
		PVOID,
		pdModule->ModuleBase,
		pNtHeaders->OptionalHeader.AddressOfEntryPoint
	);

	return TRUE;
}

其中最困難的部分是:

  • AddBaseAddressEntry
    • 其中還會用到自訂的函數 FindModuleBaseAddressIndex
      • FindModuleBaseAddressIndex
        • 從 Peb->Ldr->InLoadOrderModuleList 找出 ntdll.dll 的 LDR_DATA_TABLE_ENTRY
        • 再從 LDR_DATA_TABLE_ENTRY->BaseAddressIndexNode 找出 ModuleBaseAddressIndex
        • ModuleBaseAddressIndex 指向一個 RB Tree
    • 比對 BaseAddress,找到該插入的 module base address 在 RB Tree 中的位置後,使用 RtlRbInsertNodeEx 將剛剛創立的 module base address 插入
  • AddHashTableEntry
    • 其中還會用到自訂的函數 FindHashTable 和 LdrHashEntry
      • FindHashTable
        • 從 Peb->Ldr->InInitializationOrderModuleList 找出 Hash list 的頭就會是 Hash Table
      • LdrHashEntry
        • 使用 pRtlHashUnicodeString 回傳 BaseDllName 的 hash
        • 以 hash 為 index,在 Hash Table 中寫入 LDR_DATA_TABLE_ENTRY->HashLinks
    • 最後,將分別加入 InLoadOrderModuleListInMemoryOrderModuleList , InInitializationOrderModuleList

5. Execute the TLS callbacks

這個階段會做以下步驟:

  1. 使用 NtProtectVirtualMemory 調整 section 的 page 屬性
  2. FlushInstructionCache
  3. 執行 NT Header→OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS] 中的 TLS callbacks
	// trigger tls callbacks, set permissions and call the entry point
	if (!BeginExecution(dModule))
	{
		dModule->ErrorMsg = (wchar_t*)pHeapAlloc(pGetProcessHeap(), HEAP_ZERO_MEMORY, 500);
		if (!dModule->ErrorMsg)
			goto Cleanup;

		pwsprintfW(dModule->ErrorMsg, TEXT("Failed to execute: %s"), lpwName);
		goto Cleanup;
	}

	dModule->bSuccess = TRUE;

	goto Cleanup;

	Cleanup:
		return dModule;

6. Register the exception handlers

在 Windows 64 bits 的環境,使用 RtlAddFunctionTable 將 NT Headers→OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION] 中的 FuncEntry 加入 SEH 的 FunctionTable

7. Call the DLL entry point (DllMain)

最後,執行 DLL 的 entrypoint

Result

測試看看 Autoruns64.dll 可不可以被載入

VOID main()
{
	GETPROCESSHEAP pGetProcessHeap = (GETPROCESSHEAP)GetFunctionAddress(IsModulePresent(L"Kernel32.dll"), "GetProcessHeap");
	HEAPFREE pHeapFree = (HEAPFREE)GetFunctionAddress(IsModulePresent(L"Kernel32.dll"), "HeapFree");

	PDARKMODULE DarkModule = DarkLoadLibrary(
		LOAD_LOCAL_FILE,
		L".\\autoruns64.dll",
		NULL,
		0,
		NULL
	);

	if (!DarkModule->bSuccess)
	{
		printf("load failed: %S\n", DarkModule->ErrorMsg);
		pHeapFree(pGetProcessHeap(), 0, DarkModule->ErrorMsg);
		pHeapFree(pGetProcessHeap(), 0, DarkModule);
		return;
	}

	_ThisIsAFunction ThisIsAFunction = (_ThisIsAFunction)GetFunctionAddress(
		(HMODULE)DarkModule->ModuleBase,
		"AutorunScan"
	);
	pHeapFree(pGetProcessHeap(), 0, DarkModule);

	if (!ThisIsAFunction)
	{
		printf("failed to find it\n");
		return;
	}

    ThisIsAFunction(L"this is working!!!");
	while (1);

	return;
}

https://ithelp.ithome.com.tw/upload/images/20230927/20120098eFfaf7sUgo.png

成功在 console 印出 autorun key!

雖然 Dark LoadLibrary 有不少地方還未實作,但是在分析完 Dark LoadLibrary 後,才發現自己實作完整版的 DLL Loader 還蠻困難的,要考慮非常多的細節。

下一篇開始,我會有一系列關於 Token & Object 的介紹,這些物件是 Windows 權限管理的核心,所有的權限的根源都來自於這些物件。

References


上一篇
[Day12] The End of The Party: Defeat Defense Evasion
下一篇
[Day14] Token & Object (Part 1): Introduction
系列文
Windows Security 10130
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

1
zeze
iT邦新手 1 級 ‧ 2023-09-28 00:18:26

強欸,幾乎是把 Windows 載入 Image 所做的行為都自己實作了。

不過我可以理解 Reflective DLL Injection 可以用來繞過 LoadImageNotifyRoutine,但讓載入的 DLL 可以用 GetProcAddress 和 GetModuleHandle 的用意是什麼?

我要留言

立即登入留言